package cn.seaboot.admin.security.login;

import cn.seaboot.admin.security.auth.SecurityAuthorityProvider;
import cn.seaboot.admin.security.token.UserToken;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.util.List;

/**
 * 授权信息查询
 * <p>
 * 密码验证、授权信息查询
 *
 * 实现方式参考自 springboot2 源代码
 *
 * @author Mr.css
 * @version 2021-12-23 10:53
 * @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
 */
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);


    /**
     * 如果密码不存在，仍然执行密码验证，防止黑客嗅探用户是否存在（SEC-2056.）
     */
    private static final String USER_NOT_FOUND_PASSWORD = "default";

    /**
     * 用于在未找到用户时执行 {@link PasswordEncoder#matches(CharSequence, String)} 的密码，以避免 SEC-2056。
     * 这是必要的，因为如果密码格式无效，某些 {@link PasswordEncoder} 实现会短路。
     */
    private volatile String userNotFoundEncodedPassword;

    /**
     * 用户信息查询
     */
    private UserDetailsService userDetailsService;
    /**
     * 密码加解密
     */
    private PasswordEncoder passwordEncoder;

    /**
     * 异常信息格式化
     */
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();


    @Resource
    private SecurityAuthorityProvider securityAuthorityProvider;
    /**
     * 是否隐藏用户不存在异常
     */
    private boolean hideUserNotFoundExceptions = true;

    private final UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    private final UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
    private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    /**
     * 标明当前Provider能够处理的Token类型
     *
     * @param authentication tokenClass
     * @return boolean
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return UserToken.class == authentication;
    }

    /**
     * 身份鉴权，最终返回完成认证的身份令牌
     *
     * @param authentication 身份证明
     * @return Authentication  已经完成的身份证明(a fully authenticated object including credentials)
     * @throws AuthenticationException e
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //  Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

        UserDetails details;

        try {
            // 获取用户信息，即便没找到，也要模拟登录流程
            details = retrieveUser(username, authentication);
        } catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");

            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            } else {
                throw notFound;
            }
        }

        Assert.notNull(details, "retrieveUser returned null - a violation of the interface contract");

        // 用户状态验证
        preAuthenticationChecks.check(details);
        // 验证密码有效性
        additionalAuthenticationChecks(details, authentication);
        // 用户状态验证
        postAuthenticationChecks.check(details);

        // 构建一个经过授权的身份令牌
        return createSuccessAuthentication(username, authentication, details);

    }

    /**
     * Creates a successful {@link Authentication} object.
     * <p>
     * Protected so subclasses can override.
     * </p>
     * <p>
     * Subclasses will usually store the original credentials the user supplied (not
     * salted or encoded passwords) in the returned <code>Authentication</code> object.
     * </p>
     *
     * @param username       that should be the principal in the returned object (defined by
     *                       the {code #isForcePrincipalAsString()} method)
     * @param authentication that was presented to the provider for validation
     * @param user           that was loaded by the implementation
     * @return the successful authentication token
     */
    protected Authentication createSuccessAuthentication(String username,
                                                         Authentication authentication, UserDetails user) {
        List<GrantedAuthority> grantedAuthorities = securityAuthorityProvider.queryGrantedAuthority(username);
        // 注意，这里取消了令牌中的密码部分
        UserToken result = new UserToken(username, null, authoritiesMapper.mapAuthorities(grantedAuthorities));
        result.setDetails(authentication.getDetails());
        return result;
    }

    /**
     * 获取用户，即便没找到用户信息，也会模拟账号密码校验的流程，
     * 用于防止黑客嗅探系统账号是否存在。
     *
     * @param username       用户名
     * @param authentication 认证信息
     * @return 用户明细
     * @throws AuthenticationException -
     */
    @NotNull
    private UserDetails retrieveUser(String username,
                                     Authentication authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    /**
     * 缓解嗅探攻击的安全保护代码
     */
    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
        }
    }

    /**
     * 缓解嗅探攻击的安全保护代码
     *
     * @param authentication 认证信息
     */
    private void mitigateAgainstTimingAttack(Authentication authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
    }

    /**
     * 身份信息验证————密码验证
     *
     * @param userDetails    用户明细
     * @param authentication 认证信息
     * @throws AuthenticationException -
     */
    private void additionalAuthenticationChecks(UserDetails userDetails,
                                                Authentication authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }


    //  getter/setter----------------------------------------------------------------

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public PasswordEncoder getPasswordEncoder() {
        return passwordEncoder;
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    public boolean isHideUserNotFoundExceptions() {
        return hideUserNotFoundExceptions;
    }

    public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
        this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
    }

    /**
     * 由使用 {@link UserDetailsService} 检查加载的 <tt>UserDetails</tt> 对象状态的类调用。
     * 检查与帐户关联的各种标志，如果信息无法使用则引发异常（例如，如果用户帐户被锁定或禁用），
     * <p>
     * 目的是该接口应仅用于检查与用户关联的持久数据。 它不应参与根据提交的身份验证请求做出任何身份验证决定。
     */
    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }

            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"));
            }

            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"));
            }
        }
    }

    /**
     * 指示用户的凭据（密码）是否已过期。 过期的凭据会阻止身份验证。
     * <p>
     * 目的是该接口应仅用于检查与用户关联的持久数据。 它不应参与根据提交的身份验证请求做出任何身份验证决定。
     */
    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                logger.debug("User account credentials have expired");

                throw new CredentialsExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                        "User credentials have expired"));
            }
        }
    }
}
